13. OOP

我们以前编程都是面向过程的,基本想一个程序就从头到尾一行一行的去撸代码就可以,但这个是远远达不到现在 软件的生产要求的,我们需要从新认识世界。

如果让我们描述一个场景,我们本能的会想到动作,这是我们的固有思维习惯,比如让你说说你学习的事情,大概的 场景应该是这样:

    1. 我想学习python
    2. 上网搜了一下下
    3. 哇嗷,好多广告呀,9.9包邮,好吃不贵,免费试吃,先尝后买
    4. 我发现北京图灵学院的python不错
    5. 免费的试试呗
    6. ......

你们发现没,上面的描述几乎所有都是动作,一连串的动作构成了一个完整的动作链,然后由他来完成某些任务,这个 思考方式很不面向对象,我们如果这么编程,面临的主要问题是:

  1. 代码复用,虽然函数也可以代码复用,但复用程度达不到现在的要求

  2. 可维护性,面向过程代码的可维护性比面向对象差很多

  3. 扩展性,面向过程后期的扩展相比面向对象可能是个灾难

事实上,我们不是有了面向对象后才喜新厌旧的,二是发现面向过程满足不了需求后才寻求新的解决方案.

扩展知识:软件危机

因笔者知识有限且思想混乱,以下函数和方法其实说的都是一个概念-function

为了寻求一种更适合的软件技术方案,我们发展出了面向对象(OOP, ObjectOrientedProgramming)。

  • 几个名词

    • OO:面向对象(ObjectOriented)

    • OOA:面向对象的分析(Analysis)

    • OOD:面向对象的设计(Design)

    • OOI:xxx的实现(Implementation)

    • OOP:xxx的编程(Programming)

    • OOA->OOD->OOI: 面向对象的实现过程

OOP首先是一种思想,一种对世界不同的认知,然后才发展出了相应的技术和方案,才有了我们现在更加五彩斑斓的软件世界。

问题是,究竟什么是面向对象编程?

所谓面向对象编程,就是把我们的世界(需求)看做一个系统,这个系统首先抽象成了有什么类型的模型,此时的模型的类型 就是类,类型具有哪些特征/属性,具有哪些动作/功能,然后在细化,我们的系统由有哪些具体的东西组成的,这个具体的东西就是所谓的实例,说不明白的事,安排个栗子吧:

任务:做一个北京图灵学院的教务系统
1. 北京图灵学院这个系统(世界)由什么类型的模型组成的呢,注意是类型:
    1. 老师
    2. 学生
    3. 教室
    4. 课程
2. 考虑具体一类事物具有的特征和功能
    1. 老师:
        a. 属性类: 老师要具有姓名,地址,手机号,薪水,代的课程
        b. 功能类: 老师教课,批改作业,修改学生分数,上课
    2. 学生:
        a. 属性类: 姓名,学号, 成绩,课程
        b. 功能类: 上课,下作业,考试, 谈恋爱
    3. 教室:
        a. 属性类: 地址,房间号,承载量
        b. 功能类: 预定
    4. 课程
        a. 属性类:名称,课程号码,所有的学分
        b. 工嗯类:好像没有
3. 然后考虑由哪个具体的人/事组成,在哪个类下,比如
    1. 老师: 具体哪几位老师,张三李四王五赵麻子... ...
    2. 学生:具体有哪几个学生,张小三,李小四,王小五,赵小麻子.....
    3. 教室:有哪几间教室
    4. 课程: 有哪几门课程     
4. 这样,我们制作这个教务系统的时候,先准备好模型,比如老师模型,这个模型是抽象的,代表一类人,这类人叫老师,会教课,然后我们考虑
由哪些具体老师组成,这个时候的老师就是具体的某个人,比如刘大拿,这个时候我们只要让老师的模型生产出一个具体的老师-刘大拿就可以了。

面向对象首先需要考虑哪些类,这个时候所谓的类就是表示抽象一个群体的名词,比如老师,学生,课程,教室,这些词分别表示一类事务,而且是抽象的,就是 说你明白什么意思,也知道它应该有什么,比如老师需要教课,备课等功能,应该具有姓名,职称等属性,但你却不能说他姓名叫什么,他讲哪门课,因为老师是个 抽象的名称,描述的是一类人。这种名称我们叫做类,也是将来生产某个具体老师的模具。

而根据模具生产出来的某个具体的老师,就叫做实例,或者对象。

13.1. 面向对象基础

如何定义一个类? 我们大概有这么几种,如果不考虑继承的情况下,有大概三种,

    # 以前车马都很慢,定义一个类也很简单
    class TulingOne:
        # 函数和属性列表
        pass

    # 后来长大啦,变得复杂些
    class TulingTwo():
        # 函数和属性列表
        pass

    # 再后来成熟了,彻底黑化了
    class TulingThree(object):
        # 函数和属性列表
        pass

以上三种分别是定义类的三个形式(假定没有继承), 其中:

  1. class是关键字,表示用来定义一个类

  2. 类名称,Python规范要求使用大驼峰命名法,即英语单词组成,首字母大写。

  3. 父类列表,如果没有父类,如上面代码,则可以

    • 不写小括号

    • 写一个空括号

    • 写小括号,小括号内写objectobject是所有类的顶级父类,暂时不解释

    • 推荐使用第三种作为习惯用法

  4. 属性或者函数列表,如果有的话

上面是定义一个类的大致方式,推荐第三种作为习惯用法。

一个类代表一类抽象的事务集合,比如老师这个群体,一个这样的抽象事务一般包括两种信息:

  • 一类为普通信息,我们称之为属性,比如老师应该具有姓名,年龄,爱好等

  • 一类是功能或者动作,我们一般叫做类函数

一个比较真实一点的类的案例如下:

    # 定义老师类
    class Teacher(object):
        # 具有属性name和age
        name = "北京图灵学院的老师"
        age = 18
        
        def teaching(self):
            # 类函数
            # 代表一个功能
            print("在讲课... ...")

上面案例中,定义了老师类Teacher, 老师应该具有两个属性,此处注意属性的定义,跟我们平常定义属性基本一致。

在定义类函数teaching的时候,需要自带函数self,这个函数是类函数需要有的,具体含义后面会有解释,除了这个参数, 其余的类函数的定义跟普通函数一样。

13.1.1. 类的实例化和属性/函数的使用

实例化一个类可以理解成在有了一个模型基础上按照这个模型生产具体的成员的过程。

利用我们上面定义的老师类,我们可以生成几个具体的老师,因为Teacher类表示抽象的类别,通过实例化就可以有几个具体的老师啦。

下面代码首先生成了一个实例,这个过程也叫实例化,得到一个具体的老师,这个老师用变量teacher_wang代表。

通过这个实例,我们可以访问他的属性和功能。 访问实例/类的属性和功能,需要通过点号操作 ., 此处的点号可以看做是属于的意思,即 后面的属性或者函数属于点号前面的实例或类所有。

在定义类的时候我们给属性进行了赋值,此时的赋值当做默认值,即任意一个这个类的实例都具有这样的属性和值,定义的属性我们还可以更改,更改通过 点号操作直接赋值即可。

还可以调用实例的函数,此时如果通过实例调用函数,则参数self不需要有的,如果有其他参数直接传入参数,没有的话空着参数列表即可。

    # 生成一个具体的老师
    teacher_wang = Teacher()

    # 这个老师有默认的名字和年龄,但我买可以更改
    # 写法是实例名后面用点号表示
    print(teacher_wang.name)
    print(teacher_wang.age)


    teacher_wang.name = "刘大拿"
    teacher_wang.age = 19

    # 调用实例的功能
    # 此时调用不需要参数
    teacher_wang.teaching()


    print(teacher_wang)
    print(teacher_wang.name)
    print(teacher_wang.age)

运行结果如下:

    北京图灵学院的老师
    18
    在讲课... ...
    <__main__.Teacher object at 0x7fd0d8dfad68>
    刘大拿
    19

13.1.2. self的含义

在类定义函数的时候,一般需要第一个参数为self, 此类函数叫做实例函数, 此时代表的意思是当调用次函数的时候,需要用一个 实例在点号前面,而系统会默认把这个实例作为参数传入到函数中去,self就代表传入的实例,作为实例它具有属性,功能等, 但是在调用的时候如果是用实例调用,就不需要写出来了,系统自动处理。

  • self的名称不唯一,可以用别的变量名称,但一般习惯应这个

  • 实际上系统会把第一个参数作为代表实例自身的参数使用,即系统把实例自己赋值给第一个参数,此时如果定义的时候没有参数,用实例调用的时候会报错

  • 如果直接使用类的名称调用实例函数也可以,此时需要手动把一个具体实例作为第一个参数传入,效果跟实例直接调用一样

参看下面案例:

class Teacher(object):
name = "北京图灵学院的老师"
age = 19

def intro(self):
    # self代表一个实例
    # 打印出实例的name和age
    print("我叫 {}".format(self.name))
    print("我今年 {} 岁".format(self.age))
    
dana = Teacher()
dana.name = "刘大拿"
dana.age = 19

# 调用函数
dana.intro()

# 效果一样
Teacher.intro(dana)

上面案例连续调用两个intro, 分别用了实例变量和类名称,注意两者区别,效果一样。 运行效果许下:

    我叫 刘大拿
    我今年 19 
    我叫 刘大拿
    我今年 19 

下面例子注意看对函数sayAgain的调用,充分说明self比较特殊只是因为他是类函数的第一个参数而已,这个参数可以叫任何允许的名字:

    class Student():
        name = "dana"
        age = 18
        
        # 注意say的写法,参数由一个self
        def say(self):
            self.name = "aaaa"
            self.age = 200
            print("My name is {0}".format(self.name))
            print("My age is {0}".format(self.age))
            
        #第一个参数就代表自己,至于叫啥,NOT IMPORTENT
        def sayAgain(s):
            print("My name is {0}".format(s.name))
            print("My age is {0}".format(s.age))
              
    yueyue = Student()
    yueyue.say()
    yueyue.sayAgain()

运行结果一样一样滴:

    My name is aaaa
    My age is 200
    My name is aaaa
    My age is 200

13.1.3. 构造函数

在对类进行实例化的时候,经常需要给类一些初始值,这些只可以通过构造函数完成。

构造函数就是在实例化的时候第一个被调用的函数,这个函数有特定的名称和功能,一般完成一些实例化的初始化工作,特别注意的实际它的调用时机, 是在实例化的过程中第一个被调用的功能。 构造函数特点:

  1. 名称和写法固定, 叫__init__, init前后两个下划线, 一个参数必须有且是self,代表构造出的那个实例本身,虽然此时实例化并未完成。

  2. 调用时机特殊, 是在实例化的过程中第一个被调用的功能。

  3. 功能特殊,一般只完成初始化工作

  4. 函数不能有返回值。

  5. 实例化的时候会被自动调用。 我们上面的例子都没有定义,也就是说构造函数可以没有,如果有。则实例化的时候第一个被调用。

     class Teacher(object):
         def __init__(self, name, age):
             self.name = name
             self.age = age
             print("初始化完毕!")
    
         def intro(self):
             # self代表一个实例
             # 打印出实例的name和age
             print("我叫 {}".format(self.name))
             print("我今年 {} 岁".format(self.age))
    
     dana = Teacher('刘大拿', 19)
    
     # 调用函数
     dana.intro()
    

上面案例运行结果是:

初始化完毕!
我叫 刘大拿
我今年 19 岁

上面代码需要注意的是:

  1. 我们没有手动调用构造函数,但在实例化的时候自动调用了

  2. 在实例化的过程中, 我给了参数,参数格式需要跟构造函数一致,除了没有第一个参数

  3. 实例化后再用实例调用函数的时候,已经有了自己的值

13.2. OOP进阶

在本章中我们尝试区分一些比较立即的定义即可,按小节进行区分。

13.2.1. 实例属性和类属性

  • 类属性和实例属性的区别

    我们在定义的时候,有类属性和实例属性的区别,区别主要是在类定义里引用属性的时候前面是否有self字样。

      class A():
          # 类变量,这两个变量属于类
          name = "dana"
          age = 18
    
          # 注意say的写法,参数由一个self
          def say(self):
              self.name = "aaaa"
              self.age = 200
    
      # 此案例说明
      # 类实例的属性和其对象的实例的属性在不对对象的实例属性赋值的前提下,
      # 指向同一个变量
    
      # 此时,A称为类实例
      print(A.name)
      print(A.age)
    
      print("*" * 20)
    
      # id可以鉴别一个变量是否和另一个变量是同一变量
      print(id(A.name))
      print(id(A.age))
    
      print("*" * 20)
      a = A()
    
      a.age = 19
      print(a.name)
      print(a.age)
      print(id(a.name))
      print(id(a.age))
    

    以下是运行结果,从结果看出,变量name类变量和实例变量本部就是一共,而变量age不一样,因为我们对实例变量age进行了写操作/赋值:

      dana
      18
      ********************
      140606634146704
      4366714464
      ********************
      dana
      19
      140606634146704
      4366714496
    

    类在实例化的时候,可以理解成是按照自己的模具生产 一个产品,但在生产的时候他偷了一个懒,那就是生产出来的产品的属性 跟类的属性共用一个,即实例的变量也同样指向了类的属性,也就是为什么上例中name的实例变量和类变量是一个的原因。

    而如果我们对类变量进行了写操作,此时则自动给实例自己生成了一个变量,例如我们的变量age, 当对它复制的时候,这个时候 实例才真正有了自己的变量,通过比较id我们发现其实他跟类变量已经不是一个了。

  • 类属性/方法的使用

    那么如何对类变量进行操作呢? 很简单了,直接类名后面写变量就好,例如下面:

      print(A.age)
      print(A.name)
      A.age = 20
      print(A.age)
    
      aa = A()
    
      print(aa.name)
      print(aa.age)
    

    上面代码我们直接使用了类变量,还对其进行了更改,更改后因为模具改变了,所以生产出来的产品自然也变了:

      dana
      20
      dana
      20
    

    下面的案例能更好的的说明了在定义一个实例的时候,究竟哪些属性属于这个实例的,哪些属于类的:

      # 此时,A称为类实例
      print(A.name)
      print(A.age)
    
      print("*" * 20)
    
      # id可以鉴别一个变量是否和另一个变量是同一变量
      print(id(A.name))
      print(id(A.age))
    
      print("*" * 20)
      a = A()
      # 查看A内所有的属性
      print(A.__dict__)
    
      #没有对实例变量进行写操作的时候,他其实是借用的类变量
      print(a.__dict__)
    
      a.name = "yaona"
      a.age = 16
    
      # 实例变量进行写操作后,实例才有自己的变量
      print(a.__dict__)
    
      print(a.name)
      print(a.age)
      print(id(a.name))
      print(id(a.age))
    

    运行结果如下:

      dana
      18
      ********************
      140666260313176
      93936169025280
      ********************
      {'__module__': '__main__', 'name': 'dana', 'age': 18, 'say': <function A.say at 0x7fef6a799730>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
      {}
      {'name': 'yaona', 'age': 16}
      yaona
      16
      140666251554240
      93936169025216
    

13.2.2. 类中定义的四种方法

一个类中可以定义的方法可以分为四种,分别是:

  1. 普通函数

  2. 实例函数

  3. 类函数

  4. 静态函数

我们挨个解释下这四个是个什么玩意儿,解释之前我们先把四个函数都定义一下,看下面代码:

class School(object):
    name = "一个学校"
    addr = "SomeWhere"
    
    # 构造函数
    def __init__(self, name="NoName", addr="NoPlace"):
        self.name = name
        self.addr = addr
    
    # 普通函数
    def hello_world():
        print("Hello world")
    
    # 普通函数2
    def say_again(name='WHO'):
        print("SayAgain")
        
    # 实例函数
    def get_name(self):
        print(self.name)
    
    # 类函数
    @classmethod
    def get_class_name(cls):
        print(cls.name)
        
    # 静态函数
    @staticmethod
    def get_slogan():
        print("我爱王晓静")
         
# 定义一个实例
tuling = School("北京图灵学院", "北京海淀区中关村北路1号")
  • 普通函数

    这类函数本质上跟类应该没什么太大关系,只是放在了类里面,可能处于代码整洁的考虑等等,定义的时候可以有参数,但参数,但调用的时候要注意用法的区别。

    调用普通函数建议采用ClassName.function_name()的方式,比较不容易误会,我们看下调用案例:

      ### 对类内普通函数的调用
    
      # 用类名加点号
      School.hello_world()
      # 如果用调用实例肯定不行的,报参数错误
      # 因为此时默认把实例作为第一个参数传递,实际上函数没有参数
      # tuling.hello_world()
    
      # 调用普通函数2可以用实例,这只是一个美丽的误会
      School.say_again()
      tuling.say_again()
    

    注意观察代码tuling.say_again(), 此时调用能够成功仅仅是因为实例调用函数的时候,系统默认把实例自己当做第一个参数传入,正好参数个数匹配。所以 没有报错,但这其实不是我们想要的结果,只是因为Python不做严格类型检查而已,也是容易犯错误的地方。

  • 实例函数

    这个函数是我们使用类的时候常用的函数,此类函数显著特征是第一个参数代表类实例自己,调用的时候如果用实例调用,则系统默认把实例作为第一个参数传入, 记住这个动作是是系统自动实现,所以你调用的时候要少传一个参数。

    此类函数调用也可以用类名调用,但用类名调用没有实例调用的默认行为,即不会自动填充第一个参数,所以我们需要手动把第一个参数位置放入一个实例。

    下面是代码调用代码案例:

      ### 调用实例函数
    
      # 正经人调用实例函数的方法
      tuling.get_name()
      # 邪恶的人调用实例函数也可以这样,不推荐
      School.get_name(tuling)
    

    上面运行结果就是打印两个北京图灵学院, 道理很简单。

  • 类函数

    实例函数第一个参数是self代表实例自己,调用的时候系统自动把实例作为第一个参数传递,类函数类似,只不过第一个参数一般用cls代表类自己, 调用的时候也默认把类自己作为第一个参数传入。

    类函数定义用语法糖@classmethod修饰,调用的时候可以用实例调用,效果一样, 参考下面代码:

      ### 调用类函数
      # 系统自动把School作为第一个参数传入
      School.get_class_name()
      # 系统自动把tuling作为第一个参数传入
      # Python比较傻,傻傻就分不清了,就认为你对
      tuling.get_class_name()
    

    上面两次调用效果一致,特别是用实例调动的时候,此时系统默认把tuling作为第一个参数传入,传入的时候系统当做实例, 结果get_class_name 却把它当做一个类接收并使用,误会了,误会了而已。

    代码执行结果一致,都是一个学校, 是类的属性:

      一个学校
      一个学校
    
  • 静态函数

    静态函数定义的时候用语法糖@staticmeethod修饰,参数没有代表类或者实例的参数,调用的时候也不会自动传入啥。

      ### 调用静态函数
    
      School.get_slogan("王晓静")
      # 实例调用的时候也不默认把实例作为参数传入
      tuling.get_slogan("王晓静")
    

    注意第二次调用,实例调用函数一般把自己作为第一个参数默认传入,但此时代码显示结果证明并没有发生这样的事情,要想用实例掉好用成功必须手动传入。

    静态函数在编程理论值属于一个专有名词,此处可以参考下

13.2.3. 类函数和静态函数的区别

静态函数基本属于编程理论的一个专业名词,但Python中的静态函数跟编译原理中的静态不太一样:

  • Python中对静态函数的时候用更多的是把静态函数看做一个执行跟类无任何关系的功能块,之所以把他放入某一个类中,更多的考虑是命名空间的问题,即处于代码管理的 目的,总得把它放到换一个地方

  • 类函数一般的使用是把它作为一个类似JavaFactory设计模式的使用,即用来产生一个类实例

  • 举个栗子吧

    一般创建类是需要用__new__或者__init__构造,但有时候因为条件不符合,这个时候我们也想创建一个类,此时就可以用类函数来操作,下面栗子是从网上抄来的, 设计的很精妙:

      class Date(object):
    
          def __init__(self, day=0, month=0, year=0):
              self.day = day
              self.month = month
              self.year = year
    
          @classmethod
          def from_string(cls, date_as_string):
              day, month, year = map(int, date_as_string.split('-'))
              date1 = cls(day, month, year)
              return date1
    
          @staticmethod
          def is_date_valid(date_as_string):
              day, month, year = map(int, date_as_string.split('-'))
              return day <= 31 and month <= 12 and year <= 3999
    
      date2 = Date.from_string('11-09-2012')
      is_date = Date.is_date_valid('11-09-2012')
    

    上例中, 对于Date类的初始化需要年月日三个数值,但我们偶尔也会出现字符串形式的日期格式,这个时候处理起来就比较困难,因为Python不支持多个构造函数, 所以使用类函数来处理比较方便,类函数第一个参数传入的是类本身。在类函数中把字符串处理成数字后刚好利用默认传入的类来构造实例,这样就可以通过 类函数来生成一个相应的实例了,行为和结果是类似java中工厂函数。

13.3. OOP的三大特性

在学习OOP的时候,我们通常会说OOP有三大特征,即:

  • 封装

  • 继承

  • 多态

下面我们尝试挨个去解读。

13.3.1. 封装

所谓封装就是对对象或者类的内容的访问进行控制,规定谁可以访问什么内容。

通常来说,我们会把的内容分为三类,即:

  • 公开的,public

  • 受保护的,protected

  • 私有的,private

Java等静态编程语言不同的是,这里面的public,private,protected不是关键字, 仅仅是我们用来形容对象内容访问权限的三个普通的词而已。

  • 定义: 定义类的私有内容,需要在变量或者函数名称前加__, 即两个下划线。

  • 使用: Python并不完全进制外部访问类的私有内容,但推荐不采用外部访问的形式

    • 如果是类或者实例访问,直接采用点号就可以

    • 如果是外部访问,因为Python采用了一种 name mangling技术,即默认把私有内容改了个名字,规则是__name编程_ClassName__name, 注意是 以单下划线开头

        class Teacher(object):
            name = "NoName"
            __age = 18
      
            def __init__(self, name="MyName", age=18):
                self.name = name
                self.__age = age
      
            def print_info(self):
                print("内部使用名称:",self.name)
                print("内部使用年龄:",self.__age)
      
        t = Teacher("刘大拿", 19)
      
        print("老师的名字是:", t.name)
      
        # 此时访问 __age会报错
        # print("老师的年龄是:", t.__age)
      
        # 如果必须访问,需要在前面加上 _Teacher, 注意是一个下划线
        print("老师的年龄是:", t._Teacher__age)
      
        # 访问类变量同理
        print("类名称: ", Teacher.name)
      
        # 访问类私有变量
        print("类私有: ", Teacher._Teacher__age)
      
        # 会报错
        # print("类私有: ", Teacher.__age)
      
      
        # 内部使用是不需要name mangling的
        t.print_info()
      

      上面内容需要注意对私有变量__age的使用方式,在类内部的使用方式self.__age和在类外部的使用方式t._Teacher__age是不一样的,虽然是 同一个变量。

  • 对私有属性的name mangling只在对类的定义的时候发生一次,其后创建的私有内容保持不变, 请看下面案例:

      class A(object):
          __aa = 0
    
      # 显示A的所有属性,私有属性__aa被改名了    
      print("A的所有属性,包括私有属性:", A.__dict__)
    
      ## 如果再对A添加私有属性,则不改名
      A.__bb = 100
      print("A创建后再添加私有属性,则不改名: ", A.__dict__)
    

    上面案例执行后显示结果如下,可以看出如果在类定义后再添加私有属性,则不改名:

      A的所有属性,包括私有属性: 
      {'__module__': '__main__', '_A__aa': 0, '__dict__': <attribute '__dict__' of 'A' objects>, 
      '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
    
      A创建后再添加私有属性,则不改名:  {'__module__': '__main__', '_A__aa': 0, '__dict__': <attribute '__dict__' of 'A' objects>, 
      '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None, '__bb': 100}
    

13.3.2. 类的继承

继承的社会含义一般是指我们从前辈那里靠等就能得到很多东西的行为。

在编程语言里,继承是指一个类可以获得另外一个类中的成员属性和成员的一种方法。原来就具有 相应的属性或者方法的类通常称作父类或基类,继承属性或者方法的类叫做子类或派生类,其余还有 延伸的祖父类或者孙类的概念,参考人类血缘备份体系。

继承必须满足is-a关系, 即子类必须是父类的一部分类别,例如我们定义一个类代表狗,则泰迪 继承自狗的类,这样关系是可以的,因为泰迪首先是狗,同样道理,热狗就不太适合作为狗类的子类, 虽然渣人也能算是人,但热狗真的不是狗。

继承的主要作用是减少代码重复,增加代码复用。

13.3.2.1. 类继承中的方法

类继承中的方法是在类定义的时候类名后面的小括号内写入父类的名称,参考下面代码:

class Animal():
    name = "动物"
    age = 0
    def sayHello(self):
            pass
    
class Dog(Animal):
        
    def sayHello(self):
        print("汪~~~")

class Person(Animal):
    def sayHello(self):
        print("我爱王晓静~~~")
        
caolvchong= Animal()
print("动物的名字:", caolvchong.name)
caolvchong.sayHello() #这个函数是可以调用的,虽然没结果
print("*" * 10)

corgi = Dog()
print("狗的名字:", corgi.name)
corgi.sayHello() 
print("*" * 10)

gangdan = Person()
print("人的名字:", gangdan.name)
gangdan.sayHello() 

上面代码的运行结果如下, 我们分别定义了Animal, Dog, Person三个类,其中狗和人都属于动物,这里就作为动物类的子类,在人类和狗类的定义中,没有 定义属性,父类中的属性就会自动被子类继承。

因为继承,此处两个类即拥有了name, age两个属性, 函数sayHello也会被自动继承,但子类中也定义了自己的sayHello函数, 此处子类中的函数就自动覆盖了继承下来的函数(后面会讲),所以呈现出了如下结果:

动物的名字: 动物
**********
狗的名字: 动物
~~~
**********
人的名字: 动物
我爱王晓静~~~

关于类的继承,我们需要注意的几点有:

  • 所有的类都是继承自object类,即所有的类都是object类的子类

  • 子类一旦继承父类,则可以使用父类中除私有成员外的所有内容

  • 子类继承父类后并没有将父类成员完全赋值到子类中,而是通过引用关系访问调用

  • 子类中可以定义独有的成员属性和方法,也就是说子类不但可以继承,还可以发扬光大

  • 子类中定义的成员和父类成员如果相同,则优先使用子类成员

  • 子类如果想扩充父类的方法,可以在定义新方法的同时访问父类成员来进行代码重用

关于基础中属性和函数的查找顺序:

  • 优先使用子类定义的方法和属性

  • 如果子类中没有,则自动向上(父类)查找

  • 如果查找到最高的父类也没有定义调用的属性和函数,则报错

13.3.2.2. 父类属性/函数的使用

调用类的函数或者属性,默认调用的是子类的内容,如果需要访问父类的属性或者函数,使用父类的内容有两个方法:

  • 可以使用 父类名.父类成员 的格式来调用父类成员

  • 也可以使用super().父类成员 的格式来调用

      # 子类和父类定义同一个名称变量,则优先使用子类本身
      class Person():
          name = "NoName"
          age = 18
          __score = 0 # 考试成绩是秘密,只要自己知道
          _petname = "sec" #小名,是保护的,子类可以用,但不能公用
    
          def sleep(self):
              print("Sleeping ... ...")
    
    
      # 父类写在括号内
      class Teacher(Person):
          teacher_id = "9527"
          name = "DaNa"
          def make_test(self):
              print("attention")
    
      t = Teacher()
      print(t.name)
    

    因为调用的关系,优先使用子类的内容,所以此时访问属性name的值如下:

      DaNa
    

    在必须调用父类的情形中,有一类是需要扩充父类的功能, 此时子类可以调用父类的函数来让父类完成他们的工作, 然后在本函数中写自己比父类多出来的内容。

      # 子类扩充父类功能的案例
      # 人有工作有函数, 老师也有工作的函数,但老师的工作需要讲课, 即老师的工作比正常人的工作多了那么一丢丢
      # 此时我们要对人的工作进行扩充而不是直接在老师的类中写,能继承就继承点
      class Person():
          name = "NoName"
          age = 18
    
          __score = 0 # 考试成绩是秘密,只要自己知道
          _petname = "sec" #小名,是保护的,子类可以用,但不能公用
    
          def sleep(self):
              print("Sleeping ... ...")
    
          def work(self):
              print("make some money")
    
      #父类写在括号内
      class Teacher(Person):
          teacher_id = "9527"
          name = "DaNa"
          def make_test(self):
              print("attention")
    
          def work(self):
              # 扩充父类的功能只需要调用父类相应的函数
              #Person.work(self)
              # 扩充父类的另一种方法
              # super代表得到父类
              super().work()
              self.make_test()
    
      t = Teacher()
      t.work()
    

    调用老师的工作函数,我们看到Teacher.work() 会先调用父类的work()来完成一个人普通的工作,再继续完成自己老师的工作。

    代码运行结果如下:

      make some money
      attention
    

13.3.2.3. 继承中的构造函数

构造函数同样也可以被继承,也可以在子类中的构造函数调用父类的构造函数。

  • 父子类都没有构造函数

    如果父子类中都没有构造函数,则在实例化的时候,因为子类没有构造函数,则不会主动调用构造函数,所以出现任何问题。

  • 只有子类中有构造函数

    即使父类中如果没有构造函数,子类中也可以定义自己的构造函数,此时子类中的构造函数完全不受父类影响。

    此时如果子类实例化,则会调用子类的构造函数。

      class Animel():
          pass
    
      class PaxingAni(Animel):
          pass
    
      class Dog(PaxingAni):
          def __init__(self):
              print("I am init in dog")
    
      # 实例化的时候,自动调用了Dog的构造函数
      kaka = Dog()
    

    程序运行结果如下:

      I am init in dog
    
  • 父有构造函数, 子类没有构造函数

    如果子类没有构造函数,父类定义了构造函数,则子类实例化的时候,发现子类没有构造函数则自动调用父类的构造函数,知道 找到一个跟自己实例化的时候参数符合的构造函数来完成构造。

      class Animel():
          def __init__(self):
              print("Animel")
    
      class PaxingAni(Animel):
          def __init__(self):
              print("Paxing Dongwu")
    
      class Dog(PaxingAni):
          def __init__(self):
              print("I am init in dog")
    
      # 实例化的时候,自动调用了Dog的构造函数
      # 因为找到了构造函数,则不在查找父类的构造函数
      kaka = Dog()
    
      # 猫没有写构造函数
      class Cat(PaxingAni):
          pass
    
      # 此时应该自动调用构造函数,因为Cat没有构造函数,所以查找父类构造函数
      # 在PaxingAni中查找到了构造函数,则停止向上查找
      c = Cat()
    

    上面代码运行结果如下:

      I am init in dog
      Paxing Dongwu
    

    下面代码案例演示了一个报错的情形,因为子类默认调用父类的构造函数,如果参数不匹配就会报错。

      class Animel():
          def __init__(self):
              print("Animel")
    
      class PaxingAni(Animel):
          def __init__(self, name):
              print(" Paxing Dongwu {0}".format(name))
    
      class Dog(PaxingAni):
          def __init__(self):
              print("I am init in dog")
    
      # 实例化Dog时,查找到Dog的构造函数,参数匹配,不报错      
      d = Dog()
    
      class Cat(PaxingAni):
          pass
    
      # 此时,由于Cat没有构造函数,则向上查找
      #  因为PaxingAni的构造函数需要两个参数,实例化的时候给了一个,报错
      c = Cat()
    

    上面案例的运行结果如下,第一次实例化Dog的时候不会出错,第二次实例化Cat的时候报构造函数错误:

      I am init in dog
    
      ---------------------------------------------------------------------------
    
      TypeError   Traceback (most recent call last)
    
      <ipython-input-23-160de1fa11e4> in <module>()
           24 # 此时,由于Cat没有构造函数,则向上查找
           25 #  因为PaxingAni的构造函数需要两个参数,实例化的时候给了一个,报错
      ---> 26 c = Cat()
    
      TypeError: __init__() missing 1 required positional argument: 'name'
    
  • 对父类构造函数的调用

    子类构造函数如果掉用父类构造函数,有两种方法:

    • super(ClassName, self).__init__(正常参数)

    • SuperClassName.__init__(self, 父类的正常参数)

    下面是调用父类构造函数的案例:

      # 父类是B
      class C(B):
          # c中想扩展B的构造函数,
          # 即调用B的构造函数后在添加一些功能
          # 由两种方法实现
    
          '''
          # 第一种是通过父类名调用
          def __init__(self, name):
              # 首先调用父类构造函数
              B.__init__(self, name)
              # 其次,再增加自己的功能
              print("这是C中附加的功能")
          '''  
    
          # 第二种,使用super调用
          def __init__(self, name):
              # 首先调用父类构造函数
              super(C, self).__init__(name)
              # 其次,再增加自己的功能
              print("这是C中附加的功能")
    

13.3.2.4. 多继承

继承分为单继承和多继承,顾名思义,如果一个子类只有一个父类,则称为单继承,如果一个子类有多于一个的父类,则称为多继承。

很多语言禁止多继承,因为多继承会带来一些问题,但Python从语法上不禁止多继承,在实际使用中,除非必要,否则不建议使用 多继承,如果确实需要,推荐使用Mixin来完成多继承的功能。

下面代码是是一个多继承的例子,在多继承中,定义子类的括号中依次列出父类的列表,此时顺序是比较重要的,注意。

# 多继承的例子
# 子类可以直接拥有父类的属性和方法,私有属性和方法除外
class Fish():
    def __init__(self,name):
        self.name = name
    def swim(self): # 鱼会游泳
        print("i am swimming......")
        
class Bird():
    def __init__(self, name):
        self.name = name
        
    def fly(self): #鸟会飞
        print("I am flying.....")

class Person():
    def __init__(self, name):
        self.name = name
        
    def work(self): #人会工作
        print("Working........")
        
        
# 单继承的例子      
# 学生是人的子类, 人是学生的唯一父类
class Student(Person):
    def __init__(self, name):
        self.name = name
stu = Student("yueyue")
stu.work()
        
        
# 多继承的例子  
# 超人会飞,会游泳,会工作
# 注意父类的位置和顺序
class SuperMan(Person, Bird, Fish):
    def __init__(self, name):
        self.name = name

# 会游泳的人是人和鱼的子类
class SwimMan(Person, Fish):
    def __init__(self, name):
        self.name = name
        
# 超人从鸟爸爸那里继承了飞行能力,从鱼爸爸那里继承了游泳的能力
s = SuperMan("yueyue")
s.fly()
s.swim()

多继承会带来菱形继承问题,我方跟百度达成战略合作,百度会为您免费提供解答服务